purescript-halogen-realworldのcode reading
アーキテクチャを学びたいmrsekut.icon
これが目的
隅から隅まで理解したいわけではない
halogenをちゃんと理解しているわけではないので、詰まったらtutorialに戻る、という感じで読んでいく
色々解説してるっぽい記事あった
読んでみたらめちゃくちゃよかったmrsekut.icon*3
Use Cases
e.g. userをfollowする
Data
e.g. UsersやArticlesなどのEntity
Tranformations
stateから別のstateに変換する関数
小さいaction
dataを3種類に分類する
Entities
永続的なidを持つデータ
同じidを持っているなら同じものである
e.g. User, Article
Values
同じ値なら同じものである
e.g. Email, Username, List
Lifecycles
状態を表すデータ
EntityやValueを含むデータ型
具体例を見たいmrsekut.icon
設計方針
データを設計するための原則
サポートする必要のあるビジネス的なプロセスに依ってデータ型が決まり、
データ型によって、これらのプロセスを関数として実装する方法が決まる
use caseを理解していないと適切なデータ型は作成できない
適切なデータ型がないと関数が混乱する
ここの文章めちゃくちゃ良いなmrsekut.icon
newtype CustomerId = CustomerId Natural
newtypeでIdを作ることで、「idでの算術」などを不可能にする
必ずしも完全なデータ型を作る必要はない
module内に閉じ込めて、適切に関数を提供すれば安全性は担保できる
EntityとValue
ProcessとLifecycle
アーキテクチャについて
純粋な関数型プログラミングの基本原則は、エフェクトとデータを可能な限り分離すること
コードの大部分は副作用のない関数とデータとして記述されており、アプリケーションの境界でのみEffectが現れる
型クラスは、取得または処理するために使用するメカニズムではなく、操作する情報を記述するように設計する必要があります
わかるようでわからない
普通に型クラス定義したらそうならん?
具体的な接続先を意識しないようなinterfaceを定義しろということか
アクセス先がRESTでもGraphQLでもlocalStrageでも使えるようなinterfaceを型クラス内に定義する
Capability内でHalogenM st act slots msg mにだけinstance作っているのはなぜ?
これなしで書いてみたらわかるmrsekut.icon
Componentは、HalogenMを返り値にするので、その中でcapabilityのmethodを使おうと思うと、毎回liftしないといけない
単純にそれがめんどいので、HalogenMに対してinstanceを予め定義している
あー、こういうのもLayer 3的ではあるのか
code:purs(hs)
newtype LogMessage = LogMessage String
logMessage
:: forall m
. Monad m
=> m DateTime -- | How should we fetch the current time?
-> (LogMessage -> m Unit) -- | How should we write this message?
-> LogType -- | What kind of log is this?
-> String -- | What is the input string?
-> m Unit
logMessage getTime writeLog logType msg = do
t <- fmtDateTime <$> getTime
let msg' = LogMessage $ case logType of
writeLog msg'
これは、Effectにも依存していない、純粋な関数型プログラミングと言える
これを使って、AppMのinstanceを定義する
code:purs(hs)
instance LogMessages AppM where
logMessage log = logMessage log
ここで初めて、AffやEffectと関係を持つ
薄い命令形shell
AppMは通常のMonadに加えて以下のようなものもderivingしておく
code:purs(hs)
derive newtype instance MonadEffect AppM
derive newtype instance MonadAff AppM
derive newtype instance MonadStore Action Store AppM
上2つはまあわかる
一番下はMonadAskを実装しているようなもの
Storeはhalogenが提供している別のlibrary
The only tricky part is ..lのぶぶんで、Storeはtypeで定義するが、
これはinstanceを導出できない
そこでType.Equalityを利用すればできる
理由はここを見よ、みたいにかいているが特に説明はない The beauty of these instances is that our logging capability is decoupled from its implementation in AppM
これは、capablity同士が独立しているから、1つの変更が他のcapablityのAppM実装に影響を与えない、という意味かな #?? Testing our new logger capability
テストの書き方
EnvはこのPRでStoreにrenameされている Store内のcommentにReduxのstoreと似ている、と書いているが、微妙に語弊あると思うmrsekut.icon
global stateと言う意味では同じだけど、実行中に更新されること無いから
あー、でもmainでの注入時にActionやreduceという名前の関数を使ってるのか。
ならredux的と言っても良さそう
一発目だけの話なので、誤解は与えそうだけど
この辺の話はstoreのlibraryを見るのが良さそう
Componentsに関して
Reactと比べるとややモノリシックな構成になる
componentも純粋に保つ
capabilityへアクセスすることもできる
型で表現すればいい
できるだけ、状態を持つcomponentではなく、純粋なHTMLとして作るのが良い
もうちょい見えてきてから再読しようmrsekut.icon*2
API系
Endpoint.purs
Request.purs
なぜ自分でRequestMethod型を定義しているのかと言うと、Data.HTTP.Methodとかのは汎用的にするために冗長になっているから
利用に過不足が無いように自分で定義する
Utils.purs
Component周りの
H.forkとかまだよくわかっていない
code:dir
.
├── assets
│ ├── index.html
│ └── logo.png
├── dev
│ ├── index.html
│ └── logo.png
├── index-dev.js
├── index.js
├── LICENSE
├── package-lock.json
├── package.json
├── packages.dhall
├── README.md
├── shell.nix
├── spago.dhall
├── src
│ ├── Api
│ │ ├── Endpoint.purs
│ │ ├── Request.purs
│ │ └── Utils.purs
│ ├── AppM.purs -- Application Monad
│ ├── Capability -- Capability (後述)
│ ├── Component
│ │ ├── HigherOrder
│ │ │ └── Connect.purs
│ │ ├── HTML
│ │ │ ├── ArticleList.purs
│ │ │ ├── Footer.purs
│ │ │ ├── Header.purs
│ │ │ └── Utils.purs
│ │ ├── Part
│ │ │ ├── FavoriteButton.purs
│ │ │ └── FollowButton.purs
│ │ ├── RawHTML.js
│ │ ├── RawHTML.purs
│ │ ├── Router.purs
│ │ ├── TagInput.purs
│ │ └── Utils.purs
│ ├── Data -- ValueとEntityの定義(後述)
│ ├── Store.purs
│ ├── Foreign
│ │ ├── Marked.js
│ │ └── Marked.purs
│ ├── Form
│ │ ├── Field.purs
│ │ └── Validation.purs
│ ├── Main.purs
│ └── Page
│ ├── Editor.purs
│ ├── Home.purs
│ ├── Login.purs
│ ├── Profile.purs
│ ├── Register.purs
│ ├── Settings.purs
│ └── ViewArticle.purs
└── test
└── Main.purs
Capability
LogMessages.purs
Navigate.purs
Now.purs
Resource
Article.purs
Comment.purs
Tag.purs
User.purs
Data
User ??
user idで識別
Userを表すEntity
loginなど認証が必要なactionに使われる
Avatar
Profile
usernameで識別
Userに関する公開情報を表すEntity
記事やコメントを書いたUserとか、followの対象を表す
Article
slugで識別
記事のEntity
It refers to an author,
Followの状態はBoolでは表さない
いいねmrsekut.icon
code:purs(hs)
data Relation
= Following
| NotFollowing
| You
自分のページということも型に含める
また、この仕様は元のrealworldの仕様と異なるため、ProfileRep型の外に定義する
ProfileRepにRelationを加えたものとして、Author型を定義する
Comment
identified by an id combined with the slug of a particular article. It also refers to an author.
Email
Log
PaginatedArray
PreciseDateTime
Route
Username
smat constructorの名前はparseなのかmrsekut.icon
mkUsernameではないんだな